﻿using System;
using System.IO;
using System.Linq;
using GeckoLib.CodeHandler;
using GeckoLib.Wii;
using MiscUtil.Conversion;
using MiscUtil.IO;
using Version = GeckoLib.CodeHandler.Version;

namespace GeckoLib {
    public class Gecko : Stream {
        public static readonly byte[] ValidVersions =
            new[] {
                CodeHandler.Version.Wii
            };
        private readonly EndianBinaryReader _reader;
        private readonly EndianBinaryWriter _writer;
        private readonly EndianBitConverter _bitConverter = new BigEndianBitConverter();
        public readonly Stream BaseStream;
        public Gecko(Stream baseStream) {
            if (baseStream == null) 
                throw new ArgumentNullException("baseStream");
            BaseStream = baseStream;
            _reader = new EndianBinaryReader(_bitConverter, BaseStream);
            _writer = new EndianBinaryWriter(_bitConverter, BaseStream);
        }

        #region Game Status / Version
        public virtual GameStatus GameStatus {
            get {
                BaseStream.Flush();
                _writer.Write(Commands.GameStatus);
                switch (_reader.ReadByte()) {
                    case 0: return GameStatus.Running;
                    case 1: return GameStatus.Paused;
                    case 2: return GameStatus.Breakpoint;
                    case 3: return GameStatus.Loader;
                    default: return GameStatus.Unknown;
                }
            }
        }
        private void GameStatusCommand(string debugName, byte command, GameStatus expectedStatus, string failName) {
            BaseStream.Flush();
            _writer.Write(command);
            var status = GameStatus;
            if (status != expectedStatus)
                throw new OperationFailedException(failName);

        }
        public virtual void Pause() {
            GameStatusCommand("Pausing", Commands.FreezeGame, GameStatus.Paused, "Pause");
        }
        public virtual void Resume() {
            GameStatusCommand("Resuming", Commands.UnfreezeGame, GameStatus.Running, "Resume");
        }
        public virtual void TogglePause() {
            var gameStatus = GameStatus;
            switch (gameStatus) {
                case GameStatus.Paused: Resume(); return;
                case GameStatus.Running: Pause(); return;
            }
        }
        public virtual byte Version {
            get {
                BaseStream.Flush();
                _writer.Write(Commands.Version);
                return _reader.ReadByte();
            }
        }

        public virtual bool HasValidVersion {
            get {
                var version = Version;
                return ValidVersions.Any(v => version == v);
            }
        }

        #endregion

        #region Poke
        //The base method for the other Poke methods
        private void Poke(UInt32 address, UInt32 value, byte pokeCommand) {
            switch (pokeCommand) {
                case Commands.PokeByte: break;
                case Commands.Poke16: address &= 0xFFFFFFFE; break;
                case Commands.Poke32: address &= 0xFFFFFFFC; break;
                default: throw new ArgumentOutOfRangeException("pokeCommand");
            }
            BaseStream.Flush();
            _writer.Write(pokeCommand);
            _writer.Write((((UInt64)address) << 0x20) | value);
        }
        //TODO: Research how to document an overloaded method
        public virtual void Poke(UInt32 address, Byte value) {
            Poke(address, value, Commands.PokeByte);
        }
        public virtual void Poke(UInt32 address, UInt16 value) {
            Poke(address, value, Commands.Poke16);
        }

        public virtual void Poke(UInt32 address, UInt32 value) {
            Poke(address, value, Commands.Poke32);
        }
        #endregion

        #region Hooking
        //TODO: Test this
        /// <summary>
        /// TODO
        /// </summary>
        /// <param name="pause"></param>
        /// <param name="language"></param>
        /// <param name="patches"></param>
        /// <param name="hookType"></param>
        //NOTE: I have no idea what this does, so I don't know how to test it.
        public virtual void Hook(
            bool pause = false,
            Language language = Language.NoOverride,
            Patch patches = Patch.NoPatches,
            HookType hookType = HookType.VI) {
            BaseStream.Flush();
            _writer.Write((byte)((pause ? Commands.HookPause : Commands.Hook) + hookType));
            _writer.Write((byte)language);
            _writer.Write((byte)patches);
        }
        #endregion

        #region Breakpoints
        //TODO: Test this (Doesn't work so far...)
        /// <summary>
        /// TODO
        /// </summary>
        public virtual void Step() { _writer.Write(Commands.Step); }
        ///TODO
        //Initialise a basic data breakpoint
        //address = Which address should the breakpoint be added on
        //bptype = how many bytes need to be added to the 8 byte aligned address - 5 for read, 6 for write, 7 for rw
        //exact = only break if the exact address is being accessed
        public virtual void Breakpoint(UInt32 address, BreakpointType type, bool exact = true) {
            BaseStream.Flush();
            switch (type) {
                case BreakpointType.Execute:
                    //Execute breakpoints require a different command and different parameters
                    _writer.Write(Commands.BreakpointExecute);
                    //Unlike Data breakpoints Execute breakpoints are exact to 4 bytes
                    _writer.Write(address & 0xFFFFFFFC | (byte)type);
                    return;
                case BreakpointType.Read:
                case BreakpointType.Write:
                case BreakpointType.ReadWrite:
                    address &= 0xFFFFFFF8 | (byte)type;
                    //Actual address to put the breakpoint - the identity adder is applied to it
                    if (exact && HasValidVersion) {
                        _writer.Write(Commands.BreakpointExact);
                        _writer.Write(((UInt64)address) << 32 | address);
                    } else {
                        _writer.Write(Commands.BreakpointReadWrite);
                        _writer.Write(address);
                    }
                    return;
                default:
                    throw new ArgumentOutOfRangeException("type");
            }
        }
        public virtual bool BreakpointHit { get { return GameStatus == GameStatus.Breakpoint; } }
        //Cancels running breakpoints
        //doesn't work thanks to a malfunction of current gecko handlers! //NOTE: Wat
        public void CancelBreakpoints() {
            _writer.Write(Commands.CancelBreakpoints);
        }
        #endregion

        #region Memory Transfers
        //TODO BLAH
        #endregion
        public void OpenReadMemory(uint start, uint count) {
            BaseStream.Flush();
            _writer.Write(Commands.ReadMemory);
            if (_reader.ReadByte() != TransferResponses.Acknowledge)
                throw new OperationFailedException("ReadMemory","Gecko did not acknowledge ReadMemory request.");

        }

        public uint BytesToTransfer { get; private set; }
        public uint BytesTransferred { get; private set; }
        public bool IsTransferring { get; private set; }
       public override int Read(byte[] buffer, int offset, int count) {

            throw new NotImplementedException();
        }

        public override void Write(byte[] buffer, int offset, int count) {
            throw new NotImplementedException();
        }

        public override bool CanRead { get { return true; } }
        public override bool CanWrite { get { return true; } }

        #region Unsupported Operations
        public override bool CanSeek { get { return false; } }

        public override long Length { get { throw new NotSupportedException(); } }

        public override long Position {
            get { throw new NotSupportedException(); }
            set { throw new NotSupportedException(); }
        }
        public override long Seek(long offset, SeekOrigin origin) {
            throw new NotSupportedException();
        }

        public override void SetLength(long value) {
            throw new NotSupportedException();
        }
        public override void Flush() { throw new NotSupportedException(); }
        #endregion
    }
}
